/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using System.Xml.XPath;
using DaveSexton.Controls;

namespace DaveSexton.DocProject.Sandcastle.TopicManagement
{
	internal sealed partial class TopicExplorerControl : UserControl, ITopicHierarchy
	{
		#region Public Properties
		public TopicManager Manager
		{
			get
			{
				return manager;
			}
			set
			{
				if (manager == value)
					return;

				manager = value;

				if (manager == null)
				{
					ToggleUIState(false);

					topicXmlTreeView.DataSource = null;
				}
				else
				{
					ToggleUIState(true);

					topicXmlTreeView.DataManager = manager.DataManager;
				}
			}
		}

		[Category("Behavior"), DefaultValue(true)]
		public bool CanEdit
		{
			get
			{
				return canEdit;
			}
			set
			{
				canEdit = value;
				editToolStripMenuItem.Visible = value;
				editToolStripSeparator.Visible = value;
			}
		}

		[Category("Behavior"), DefaultValue(true)]
		public bool CanShowProperties
		{
			get
			{
				return canShowProperties;
			}
			set
			{
				canShowProperties = value;
				propertiesToolStripMenuItem.Visible = value;
				propertiesToolStripSeparator.Visible = value;
			}
		}

		[Category("Appearance"), DefaultValue(true)]
		public bool FilterToolStripButtonVisible
		{
			get
			{
				return filterToolStripButton.Visible;
			}
			set
			{
				filterToolStripButton.Visible = value;
			}
		}
		#endregion

		#region Private / Protected
		private TopicNode SelectedNonReferenceNode
		{
			get
			{
				TopicNode node = (TopicNode) topicXmlTreeView.SelectedNode;

				if (node == null || (node.Topic.TopicType != TopicType.Conceptual
					&& node.Topic.TopicType != TopicType.Container
					&& node.Topic.TopicType != TopicType.Html))
					node = null;

				return node;
			}
		}

		private TopicManager manager;
		private TopicNode originalNodeBeforeDrag;
		private bool canEdit = true, canShowProperties = true, droppedInEditor;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="TopicExplorerControl" /> class.
		/// </summary>
		public TopicExplorerControl()
		{
			InitializeComponent();
		}
		#endregion

		#region Methods
		private void ToggleUIState(bool enabled)
		{
			toolStrip.Enabled = enabled;
			topicXmlTreeView.Enabled = enabled;
		}

		private ConceptualTemplate PromptChooseConceptualTemplate(out Guid guid, out string fileName)
		{
			guid = Guid.Empty;
			fileName = null;

			// NOTE: Suspending updates to reference topics prevents a strange bug in the External UI, which causes the
			// entire program to become unresponsive forever while the dialog is shown if reference topics are being built 
			// asynchronously and complete while the dialog is still open.
			manager.SuspendUpdateReferenceTopics();

			try
			{
				using (ChooseConceptualTemplateDialog dialog = new ChooseConceptualTemplateDialog(manager.ConceptualManager.ConceptualTemplates))
				{
					if (dialog.ShowDialog(DocProjectEnvironment.MainWindow) == DialogResult.OK)
					{
						fileName = dialog.FileName;
						guid = dialog.Guid;
						return dialog.SelectedTemplate;
					}
					else
						return null;
				}
			}
			finally
			{
				manager.ResumeUpdateReferenceTopics();
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void ImportTopics()
		{
			manager.SuspendUpdateReferenceTopics();

			try
			{
				using (OpenFileDialog dialog = new OpenFileDialog())
				{
					dialog.Title = "Import Topics";
					dialog.Filter = "MAML Topic Files (*.aml)|*.aml|HTML Files (*.htm)|*.htm|All Files (*.*)|*.*";
					dialog.Multiselect = true;
					
					if (dialog.ShowDialog(DocProjectEnvironment.MainWindow) == DialogResult.OK)
					{
						Exception error = null;

						TopicNode selectedNode = SelectedNonReferenceNode;
						IXPathNavigable selectedElement = (selectedNode == null) ? manager.DataSource : selectedNode.DataSource;

						foreach (string file in dialog.FileNames)
						{
							ConceptualTemplate template = new ConceptualTemplate(file);

							try
							{
								if (file.EndsWith(".htm", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".html", StringComparison.OrdinalIgnoreCase))
									InsertTopic(selectedElement, selectedNode, template, Guid.Empty, TopicType.Html, null, null);
								else
									InsertTopic(selectedElement, selectedNode, template, Guid.Empty, TopicType.Conceptual, null,
										System.IO.Path.ChangeExtension(file, ConceptualTopicFileManager.TopicFileCompanionExtension));
							}
							catch (Exception ex)
							{
								error = ex;
							}
						}

						if (error != null)
							DocProjectEnvironment.ShowErrorDialog(error, "Error importing one or more topics: ");
					}
				}
			}
			finally
			{
				manager.ResumeUpdateReferenceTopics();
			}
		}

		private void InsertNewTopic(bool createFile)
		{
			ConceptualTemplate template = null;
			string fileName = null;
			Guid guid = Guid.Empty;

			if (createFile)
			{
				template = PromptChooseConceptualTemplate(out guid, out fileName);

				if (template == null)
					return;
			}

			TopicNode selectedNode = SelectedNonReferenceNode;
			IXPathNavigable selectedElement = (selectedNode == null) ? manager.DataSource : selectedNode.DataSource;

			InsertTopic(selectedElement, selectedNode, template, guid, TopicType.Conceptual, fileName ?? Resources.Text.Container, null);
		}

		private void InsertTopic(IXPathNavigable parentElement, TopicNode parentNode, ConceptualTemplate template, Guid guid, TopicType type, string topicName, string metadataFile)
		{
			IXPathNavigable element = manager.InsertNewTopic(parentElement, template, guid, type, topicName, metadataFile);

			manager.OpenConceptualTopic(element);

			XmlTreeNode node = (parentNode == null)
				? topicXmlTreeView.InsertNewNode(element)
				: topicXmlTreeView.InsertNewNode(parentNode, element);

			node.EnsureVisible();
		}

		public void NotifyDroppedNodeInEditor()
		{
			droppedInEditor = true;
		}

		public void Reload()
		{
			if (manager != null)
			{
				manager.SaveChanges(TopicManagerChanges.Toc | TopicManagerChanges.Filters | TopicManagerChanges.DynamicFilters);

				manager.LoadTopics(true);
			}
		}

		private void InitializeContextMenu(TopicNode node)
		{
			bool isTopicNode = false;
			TopicType topicType = TopicType.General;

			if (node != null)
			{
				topicType = node.Topic.TopicType;
				isTopicNode = !node.Topic.IsApiPlaceholder && topicType != TopicType.General && topicType != TopicType.Container;
			}

			editToolStripMenuItem.Enabled = isTopicNode;
			propertiesToolStripMenuItem.Enabled = isTopicNode && topicType != TopicType.Html;
			copyReferenceLinkToolStripMenuItem.Enabled = isTopicNode;
			copyConceptualLinkToolStripMenuItem.Enabled = isTopicNode;
		}

		public static string CreateReferenceLink(TopicNode node)
		{
			if (node == null)
				return null;
			else if (node.Topic.TopicType == TopicType.Conceptual)
				return string.Format(System.Globalization.CultureInfo.InvariantCulture,
					"<a href=\"{0}.htm\">{1}</a>&#160;", node.Topic.Id, System.Web.HttpUtility.HtmlEncode(node.Topic.TocTitle));
			else if (node.Topic.TopicType == TopicType.Html)
				return string.Format(System.Globalization.CultureInfo.InvariantCulture, 
					"<a href=\"{0}\">{1}</a>&#160;", System.IO.Path.GetFileName(node.Topic.File), System.Web.HttpUtility.HtmlEncode(node.Topic.TocTitle));
			else
				return string.Format(System.Globalization.CultureInfo.InvariantCulture,
					"<see cref=\"{0}\">{1}</see>&#160;", node.Topic.Id, System.Web.HttpUtility.HtmlEncode(node.Topic.TocTitle));
		}

		public static string CreateConceptualLink(TopicNode node)
		{
			if (node == null)
				return null;
			else if (node.Topic.TopicType == TopicType.Conceptual)
				return string.Format(System.Globalization.CultureInfo.InvariantCulture,
					"<link xlink:href=\"{0}\">{1}</link>", node.Topic.Id, System.Web.HttpUtility.HtmlEncode(node.Topic.TocTitle));
			else if (node.Topic.TopicType == TopicType.Html)
				return string.Format(System.Globalization.CultureInfo.InvariantCulture,
					@"<externalLink>
	<linkText>{0}</linkText>
	<linkUri>{1}</linkUri>
	<linkTarget>_self</linkTarget>
</externalLink>", System.Web.HttpUtility.HtmlEncode(node.Topic.TocTitle), System.IO.Path.GetFileName(node.Topic.File));
			else
				return string.Format(System.Globalization.CultureInfo.InvariantCulture,
					"<codeEntityReference>{0}</codeEntityReference>", node.Topic.Id);
		}

		public static void CopyReferenceLink(TopicNode node)
		{
			try
			{
				Clipboard.SetText(CreateReferenceLink(node) ?? "", TextDataFormat.UnicodeText);
			}
			catch (System.Runtime.InteropServices.ExternalException ex)
			// occurred in testing on a virtual PC (Windows XP)
			{
				Log.Exception(ex, Resources.Errors.CannotSetClipboardText);

				DocProjectEnvironment.ShowErrorDialog(ex, Resources.Errors.CannotSetClipboardText);
			}
		}

		public static void CopyConceptualLink(TopicNode node)
		{
			try
			{
				Clipboard.SetText(CreateConceptualLink(node) ?? "", TextDataFormat.UnicodeText);
			}
			catch (System.Runtime.InteropServices.ExternalException ex)
			// occurred in testing on a virtual PC (Windows XP)
			{
				Log.Exception(ex, Resources.Errors.CannotSetClipboardText);

				DocProjectEnvironment.ShowErrorDialog(ex, Resources.Errors.CannotSetClipboardText);
			}
		}

		public static void CopyAsTopicIdenifier(TopicNode node)
		{
			string value;

			if (node == null)
				value = string.Empty;
			else if (node.Topic.TopicType == TopicType.Html)
				value = node.Topic.File;
			else
				value = node.Topic.Id;

			try
			{
				Clipboard.SetText(value ?? "", TextDataFormat.UnicodeText);
			}
			catch (System.Runtime.InteropServices.ExternalException ex)
			// occurred in testing on a virtual PC (Windows XP)
			{
				Log.Exception(ex, Resources.Errors.CannotSetClipboardText);

				DocProjectEnvironment.ShowErrorDialog(ex, Resources.Errors.CannotSetClipboardText);
			}
		}
		#endregion

		#region Events
		private readonly object SelectedNodeChangedEvent = new object();
		private readonly object EditEvent = new object();
		private readonly object PropertiesEvent = new object();

		/// <summary>
		/// Event raised when the <strong>Properties</strong> context menu item is clicked for a topic.
		/// </summary>
		[Category("Action")]
		[Description("Event raised when the Properties content menu item is clicked for a topic.")]
		public event EventHandler<TopicNodeEventArgs> Properties
		{
			add
			{
				lock (PropertiesEvent)
				{
					Events.AddHandler(PropertiesEvent, value);
				}
			}
			remove
			{
				lock (PropertiesEvent)
				{
					Events.RemoveHandler(PropertiesEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="Properties" /> event.
		/// </summary>
		/// <param name="e"><see cref="TopicNodeEventArgs" /> object that provides the arguments for the event.</param>
		private void OnProperties(TopicNodeEventArgs e)
		{
			EventHandler<TopicNodeEventArgs> handler = null;

			lock (PropertiesEvent)
			{
				handler = (EventHandler<TopicNodeEventArgs>) Events[PropertiesEvent];
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when a topic is double-clicked or the <strong>Edit</strong> context menu item is clicked.
		/// </summary>
		[Category("Action")]
		[Description("Event raised when a topic is double-clicked or the Edit content menu item is clicked.")]
		public event EventHandler<TopicNodeEventArgs> Edit
		{
			add
			{
				lock (EditEvent)
				{
					Events.AddHandler(EditEvent, value);
				}
			}
			remove
			{
				lock (EditEvent)
				{
					Events.RemoveHandler(EditEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="Edit" /> event.
		/// </summary>
		/// <param name="e"><see cref="XmlTreeNodeEventArgs" /> object that provides the arguments for the event.</param>
		private void OnEdit(TopicNodeEventArgs e)
		{
			EventHandler<TopicNodeEventArgs> handler = null;

			lock (EditEvent)
			{
				handler = (EventHandler<TopicNodeEventArgs>) Events[EditEvent];
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when the <see cref="SelectedNode"/> property value changes.
		/// </summary>
		[Category("Property Changed")]
		[Description("Event raised when the SelectedTopic property value changes.")]
		public event EventHandler SelectedNodeChanged
		{
			add
			{
				lock (SelectedNodeChangedEvent)
				{
					Events.AddHandler(SelectedNodeChangedEvent, value);
				}
			}
			remove
			{
				lock (SelectedNodeChangedEvent)
				{
					Events.RemoveHandler(SelectedNodeChangedEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="SelectedNodeChanged" /> event.
		/// </summary>
		/// <param name="e"><see cref="EventArgs" /> object that provides the arguments for the event.</param>
		private void OnSelectedNodeChanged(EventArgs e)
		{
			EventHandler handler = null;

			lock (SelectedNodeChangedEvent)
			{
				handler = (EventHandler) Events[SelectedNodeChangedEvent];
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region Event Handlers
		private void topicXmlTreeView_CreatingNode(object sender, XmlTreeNodeCreatingEventArgs e)
		{
			TopicNode node = manager.CreateNode(e.Element);

			if (node == null)
				e.Cancel = true;
			else
				e.NewNode = node;
		}

		private void topicXmlTreeView_AfterSelect(object sender, TreeViewEventArgs e)
		{
			System.Diagnostics.Debug.WriteLine("After select node", "Topic Explorer");

			bool canDelete = topicXmlTreeView.CanDeleteSelectedNode;
			deleteToolStripButton.Enabled = canDelete;
			deleteToolStripMenuItem.Enabled = canDelete;

			if (originalNodeBeforeDrag == null)
				OnSelectedNodeChanged(EventArgs.Empty);
		}

		private void topicXmlTreeView_ItemDrag(object sender, ItemDragEventArgs e)
		{
			originalNodeBeforeDrag = (TopicNode) topicXmlTreeView.SelectedNode;
		}

		private void topicXmlTreeView_ItemDrop(object sender, ItemDragEventArgs e)
		{
			try
			{
				if (droppedInEditor)
				{
					droppedInEditor = false;

					// the selected node was changed to the drag node but the original node
					// should remain selected
					topicXmlTreeView.SelectedNode = originalNodeBeforeDrag;
				}
				else
				{
					// dragNode was selected automatically by the tree view, but the event was supressed so that
					// the editor wouldn't update in case the selected node was dropped inside of it.
					OnSelectedNodeChanged(EventArgs.Empty);
				}
			}
			finally
			{
				originalNodeBeforeDrag = null;
			}
		}

		private void topicXmlTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
		{
			if (canEdit)
				OnEdit(new TopicNodeEventArgs((TopicNode) e.Node));
		}

		private void topicXmlTreeView_MouseClick(object sender, MouseEventArgs e)
		{
			if (e.Button == MouseButtons.Right)
				topicXmlTreeView.SelectedNode = (TopicNode) topicXmlTreeView.GetNodeAt(e.X, e.Y);
		}

		private void treeContentMenuStrip_Opening(object sender, CancelEventArgs e)
		{
			TopicNode node = SelectedNode;

			if (node == null)
				e.Cancel = true;
			else
				treeContentMenuStrip.Tag = node;

			InitializeContextMenu(node);
		}

		private void editToolStripMenuItem_Click(object sender, EventArgs e)
		{
			if (canEdit)
				OnEdit(new TopicNodeEventArgs((TopicNode) treeContentMenuStrip.Tag));
		}

		private void propertiesToolStripMenuItem_Click(object sender, EventArgs e)
		{
			if (canShowProperties)
				OnProperties(new TopicNodeEventArgs((TopicNode) treeContentMenuStrip.Tag));
		}

		private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
		{
			topicXmlTreeView.DeleteNode((TopicNode) treeContentMenuStrip.Tag, (Control.ModifierKeys & Keys.Shift) != 0);
		}

		private void toggleNodesToolStripButton_Click(object sender, EventArgs e)
		{
			bool collapse = false;

			topicXmlTreeView.SuspendLayout();

			foreach (TreeNode node in topicXmlTreeView.Nodes)
			{
				if (node.IsExpanded)
				{
					collapse = true;
					break;
				}
			}

			foreach (TreeNode node in topicXmlTreeView.Nodes)
			{
				if (collapse)
					node.Collapse(false);
				else
					node.ExpandAll();
			}

			topicXmlTreeView.ResumeLayout(true);
		}

		private void addContainerToolStripButton_Click(object sender, EventArgs e)
		{
			InsertNewTopic(false);
		}

		private void addTopicToolStripButton_Click(object sender, EventArgs e)
		{
			InsertNewTopic(true);
		}

		private void importTopicToolStripButton_Click(object sender, EventArgs e)
		{
			ImportTopics();
		}

		private void deleteToolStripButton_Click(object sender, EventArgs e)
		{
			topicXmlTreeView.DeleteSelectedNode((Control.ModifierKeys & Keys.Shift) != 0);
		}

		private void filterToolStripButton_Click(object sender, EventArgs e)
		{
			TopicFiltersToolWindow.Instance.Visible = true;
		}

		private void refershToolStripButton_Click(object sender, EventArgs e)
		{
			Reload();
		}

		private void copyReferenceLinkToolStripMenuItem_Click(object sender, EventArgs e)
		{
			CopyReferenceLink((TopicNode) treeContentMenuStrip.Tag);
		}

		private void copyConceptualLinkToolStripMenuItem_Click(object sender, EventArgs e)
		{
			CopyConceptualLink((TopicNode) treeContentMenuStrip.Tag);
		}

		private void copyAsTopicIdentifierToolStripMenuItem_Click(object sender, EventArgs e)
		{
			CopyAsTopicIdenifier((TopicNode) treeContentMenuStrip.Tag);
		}
		#endregion

		#region ITopicHierarchy Members
		public Topic CurrentTopic
		{
			get
			{
				TopicNode topic = SelectedNode;

				if (topic == null)
					return null;
				else
					return topic.Topic;
			}
		}

		public TopicNode SelectedNode
		{
			get
			{
				return (TopicNode) topicXmlTreeView.SelectedNode;
			}
			set
			{
				topicXmlTreeView.SelectedNode = value;
			}
		}

		public TopicNode FirstNode
		{
			get
			{
				if (topicXmlTreeView.Nodes.Count > 0)
					return (TopicNode) topicXmlTreeView.Nodes[0];
				else
					return null;
			}
		}

		public TopicNode GetNodeById(string id)
		{
			return (TopicNode) topicXmlTreeView.GetNodeById(id);
		}

		public void ForEachNode(Action<TopicNode> action)
		{
			topicXmlTreeView.ForEachNode<TopicNode>(action);
		}

		public TopicNode FindTopic(TopicNode startAfter, Predicate<TopicNode> predicate)
		{
			return topicXmlTreeView.FindNode<TopicNode>(startAfter, predicate);
		}

		public TopicNode FindTopic(Predicate<TopicNode> predicate)
		{
			return topicXmlTreeView.FindNode<TopicNode>(predicate);
		}

		public TopicNode FindTopic(System.Xml.XPath.IXPathNavigable element)
		{
			return topicXmlTreeView.FindNode<TopicNode>(element);
		}

		public IEnumerable<TopicNode> FindTopics(Predicate<TopicNode> predicate)
		{
			return topicXmlTreeView.FindNodes<TopicNode>(predicate);
		}

		public TopicNode SelectNextTopic(TopicNode from)
		{
			return topicXmlTreeView.NextNode(from);
		}

		string ITopicHierarchy.CreateReferenceLink(TopicNode node)
		{
			return CreateReferenceLink(node);
		}

		string ITopicHierarchy.CreateConceptualLink(TopicNode node)
		{
			return CreateConceptualLink(node);
		}

		public void SelectNextTopic(Predicate<TopicNode> predicate)
		{
			TopicNode first = null;
			bool hasSelected = topicXmlTreeView.SelectedNode != null, passedSelected = false;

			TopicNode next = topicXmlTreeView.FindNode<TopicNode>(delegate(TopicNode topic)
			{
				if (hasSelected && !passedSelected && topicXmlTreeView.SelectedNode == topic)
					passedSelected = true;
				else if (predicate(topic))
				{
					if (first == null)
						first = topic;

					if (passedSelected)
						return true;
				}

				return false;
			});

			if (next == null)
			{
				if (first != null)
					next = first;
				else if (hasSelected)
					topicXmlTreeView.Focus();
			}

			if (next != null)
				topicXmlTreeView.SelectedNode = next;
		}

		public void BeginUpdate()
		{
			topicXmlTreeView.BeginUpdate();
		}

		public void EndUpdate()
		{
			topicXmlTreeView.EndUpdate();
		}
		#endregion
	}
}
